Skip to content

Comments

Support flashing in Secure Download Mode#990

Merged
SergioGasquez merged 1 commit intoesp-rs:mainfrom
enodylighting:secure_download_mode
Feb 2, 2026
Merged

Support flashing in Secure Download Mode#990
SergioGasquez merged 1 commit intoesp-rs:mainfrom
enodylighting:secure_download_mode

Conversation

@enody-carter
Copy link
Contributor

Fixes 726

When secure download mode is enabled, the flashing happens via the ROM UART download which does not support all commands. Utilize standard FlashBegin, FlashData, and FlashEnd when the stub is not utilized for flashing. Additionally, do not attempt to disable the watchdog or verify the flash when in secure download mode.

This implementation was based esptool's implementation. I included the write protection for base address < 0x8000 to protect the boot loader. In esptool this is allowed via a --force flag, but no such flag is present in espflash, so I defer to you on what you would like to do regarding this protection.

@enody-carter enody-carter force-pushed the secure_download_mode branch 4 times, most recently from e67bec9 to 5398109 Compare January 2, 2026 00:06
@enody-carter enody-carter force-pushed the secure_download_mode branch 2 times, most recently from 5d4f443 to 067f040 Compare January 5, 2026 14:47
@MabezDev MabezDev requested a review from Copilot January 5, 2026 15:48
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for flashing ESP32 devices in Secure Download Mode, which uses ROM UART download with restricted command availability. The implementation avoids using compression (which requires stub loader commands not available in Secure Download Mode) and instead uses standard FlashBegin/FlashData/FlashEnd commands. Additionally, the PR implements bootloader protection to prevent writes below address 0x8000 and disables watchdog disable, verify, and skip operations which rely on restricted commands.

Key Changes:

  • Implements non-compressed flash operations using FlashBegin/FlashData/FlashEnd for Secure Download Mode
  • Adds bootloader protection to reject writes below 0x8000 when Secure Download Mode is enabled
  • Skips watchdog disable, verify, and skip operations in Secure Download Mode

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
espflash/src/target/flash_target/esp32.rs Implements dual-path flashing logic (compressed with stub vs uncompressed without stub), updates watchdog disable to skip in Secure Download Mode, renames need_deflate_end to need_flash_end
espflash/src/flasher/mod.rs Adds bootloader protection checks and warning messages for Secure Download Mode in both image and binary flashing methods
espflash/src/error.rs Adds new error variant for bootloader protection violation
CHANGELOG.md Documents the new Secure Download Mode support feature

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 932 to 936
for segment in image_format.clone().flash_segments() {
if segment.addr < BOOTLOADER_PROTECTION_ADDR {
return Err(Error::SecureDownloadBootloaderProtection);
}
}
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The image_format is cloned here to iterate segments for address validation, then the original is consumed later at line 964. This could be optimized by collecting segments once into a Vec, validating addresses, then flashing from the Vec. However, this would require changing how flash_segments works since it consumes self.

Consider refactoring to collect segments once and reuse them, or provide a non-consuming iterator method for flash_segments to avoid the clone.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is valid, but also begs the question whether we should split the check out into its own fn, as its duplicated in more than one place.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I ended up not breaking it out initially as load_image_to_flash() and write_bins_to_flash() are close to duplicates right now. I'm happy break this out into a new fn and open a new PR to track a unification of these and introduce a --force flag if desired).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can do the deduplication in this PR and leave the --force flag for an upcoming PR?

Comment on lines 157 to 160
let mut data = segment.data.to_vec();
let padding = (flash_write_size - (data.len() % flash_write_size)) % flash_write_size;
data.extend(repeat_n(0xff, padding));
data
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When compression is disabled, the data is manually padded here with 0xff bytes to align to flash_write_size. However, at line 229, the FlashData command is also configured with pad_to: flash_write_size, which causes the data_command function to apply padding again. This results in double padding.

The manual padding should be removed since the FlashData command handles padding automatically via the pad_to parameter.

Copilot uses AI. Check for mistakes.
})?;
Ok(())
},
)?;
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When FlashBegin is called (non-compression path), need_flash_end should be set to true to ensure FlashEnd is called in the finish() method. Without this, the flash operation won't be properly completed in Secure Download Mode.

Add self.need_flash_end = true; after the FlashBegin command, similar to what's done for the compression path at line 183.

Suggested change
)?;
)?;
self.need_flash_end = true;

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is absolutely valid, messed up my rebase and caused this.

Copy link
Member

@MabezDev MabezDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I meant to come back and see copilots output but I got distracted. I've resolved and added some comments on relevant review items.

Comment on lines 932 to 936
for segment in image_format.clone().flash_segments() {
if segment.addr < BOOTLOADER_PROTECTION_ADDR {
return Err(Error::SecureDownloadBootloaderProtection);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is valid, but also begs the question whether we should split the check out into its own fn, as its duplicated in more than one place.

@MabezDev MabezDev added the status:awaiting-response Awaiting a response from the author label Jan 13, 2026
@MabezDev
Copy link
Member

Hey @enody-carter this is a great addition, and really close to complete! Any chance you can come back and push it over the finish line?

@enody-carter
Copy link
Contributor Author

Yes! Had a bunch of travel, but will wrap up today.

@enody-carter
Copy link
Contributor Author

enody-carter commented Jan 28, 2026

I believe I have addressed all feedback. Additionally, I tested with the following procedure:

On a secure boot enabled ESP32-C6:

  • write-bin an encrypted and signed application to 0x20000, ensure successfully written and device boot app (verified via UART logging)
  • attempt to write-bin an encrypted and signed application to 0x0000, ensure write was not attempted

On a non-secure boot enabled device:

  • attempt a write-bin to 0x0000, ensure write succeeds via flash verification.

The tests were performed on macOS 15 and Ubuntu 24.04.

Copy link
Member

@SergioGasquez SergioGasquez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks for the contribution, sadly we have no way of currently testing it on HIL, maybe this is something we could improve in the future. This would probably need a rebase once #996 is merged as the required jobs are now updated!

* With secure download enabled, the stub is not utilized so
compressed flashing is unavailble. In this situation, use the
uncompressed flashing commands.

* If secure download is enabled, prevent flashing data to addresses
below 0x8000 to prevent overwriting the bootloader and bricking the
device
@SergioGasquez SergioGasquez added this pull request to the merge queue Jan 30, 2026
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Jan 30, 2026
@SergioGasquez SergioGasquez added this pull request to the merge queue Jan 30, 2026
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Jan 30, 2026
@SergioGasquez SergioGasquez added this pull request to the merge queue Jan 30, 2026
github-merge-queue bot pushed a commit that referenced this pull request Jan 30, 2026
* With secure download enabled, the stub is not utilized so
compressed flashing is unavailble. In this situation, use the
uncompressed flashing commands.

* If secure download is enabled, prevent flashing data to addresses
below 0x8000 to prevent overwriting the bootloader and bricking the
device
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Jan 30, 2026
@MabezDev MabezDev added this pull request to the merge queue Feb 1, 2026
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Feb 1, 2026
@SergioGasquez SergioGasquez added this pull request to the merge queue Feb 2, 2026
github-merge-queue bot pushed a commit that referenced this pull request Feb 2, 2026
* With secure download enabled, the stub is not utilized so
compressed flashing is unavailble. In this situation, use the
uncompressed flashing commands.

* If secure download is enabled, prevent flashing data to addresses
below 0x8000 to prevent overwriting the bootloader and bricking the
device
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Feb 2, 2026
@MabezDev MabezDev added this pull request to the merge queue Feb 2, 2026
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Feb 2, 2026
@SergioGasquez SergioGasquez added this pull request to the merge queue Feb 2, 2026
Merged via the queue into esp-rs:main with commit 4b0cb41 Feb 2, 2026
74 of 77 checks passed
@SergioGasquez
Copy link
Member

Hi @enody-carter!

I included the write protection for base address < 0x8000 to protect the boot loader. In esptool this is allowed via a --force flag, but no such flag is present in espflash, so I defer to you on what you would like to do regarding this protection.

Mind sharing where you found this information? Afaik, in esptool, the bootloader write protection is placed when secure boot or flash encryption are enabled (see https://docs.espressif.com/projects/esptool/en/latest/esp32h2/esptool/basic-commands.html#flash-protection)

Doing esptool --port /dev/ttyUSB0 --no-stub --chip esp32h2 write_flash -fs 4MB 0 app.bin without requiring --force

@enody-carter
Copy link
Contributor Author

Hello Sergio,

There are a series of checks in esptool cmds.py (starting at line 548).

They perform a series of more specific tests (chip specialized, is encryption also enabled on Secure Boot V2), which are all gated behind the --force flag. There is a comment regarding the safety of writing the boot loader:

Check if Secure Boot V1 is active (ESP32 only)

V1 stores the signing key in chip eFuse - bootloader reflash is dangerous

V2 uses external signing key - bootloader updates are safe

but during the development / testing of this, I ended up (I think) bricking a couple devices that are using the ESP32-C6 WROOM1 (secure boot & encryption enabled) directly and do not have the BOOT pin tied to a button. This resulted in an invalid boot loader that caused a boot loop, with me unable to put a working bootloader back on.

I do not believe any sort of write protection is put in place when secure boot v2 is enabled. In the linked document, only erase is disabled, but I believe you could accomplish the same amount of bricked device via writing an invalid 2nd stage bootloader to flash. There are the various modes of secure download that tend to be enabled at the same time, but from my testing no direct write protection is enabled on the device itself.

That being said, I do think there is room for a more granular set of checks regarding the use of secure download mode, secure boot, and flash encryption. I will need to acquire more hardware for testing before I can feel confident implementing these though !

@SergioGasquez
Copy link
Member

There are a series of checks in esptool cmds.py (starting at line 548).

But the bootloader protection is only applied when using secure boot (which is not yet implemented in espflash), not sure if theres any reason to keep in espflash at the momment as it prevents using the flash subcommand (the equivalent of esptool --chip esp32c3 -p /dev/ttyUSB0 --no-stub --flash-size 2MB 0x0 bootloader/bootloader.bin 0x8000 partition_table/partition-table.bin 0x10000 hello_world.bin , which works in an sdm chip)

@enody-carter
Copy link
Contributor Author

Ah, I see. This inadvertently coupled SDM and Secure Boot. I apologize, I definitely let the details of my boot process leak into this patch.

I am working on broader support for secure boot in espflash, at which point this would be the right change. Want me to open a PR to remove the 0x8000 check until secure boot is supported?

@SergioGasquez
Copy link
Member

I am working on broader support for secure boot in espflash, at which point this would be the right change. Want me to open a PR to remove the 0x8000 check until secure boot is supported?

Thanks, looking forward to the secure boot support <3 ! Dont worry, I can remove the 0x8000 check in #1002

@SergioGasquez SergioGasquez mentioned this pull request Feb 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status:awaiting-response Awaiting a response from the author

Projects

None yet

Development

Successfully merging this pull request may close these issues.

espflash write-bin not compatible with Secure Download mode

4 participants